Ogni calcolatore è in grado di comprendere ed
eseguire un certo numero di istruzioni quando queste sono codificate nel suo
specifico
linguaggio macchina. Solitamente le operazioni fornite
riguardano:
trasferimento di dati, elaborazione di dati (aritmetiche,
logiche), test e
salto, controllo, ma è possibile trovare,
grazie alla maggior scala di integrazione, istruzioni per i calcoli grafici -
prodotto scalare - o la gestione della memoria -
memoria virtuale,
memoria protetta. Istruzioni e dati del linguaggio macchina vanno espressi
in formato binario, ma programmi appositi - gli
assemblatori - consentono
di usare nomi mnemonici (facili da ricordare) invece che numeri; sarà poi
l'assemblatore a
tradurre il linguaggio
assembly in numeri
segnalandoci i nomi errati. Anche la programmazione in assembly non è
molto agevole, poiché le istruzioni sono
a basso livello, e
ciò obbliga a usarne molte solo per esprimere una semplice operazione: la
somma C=A+B richiede di copiare A e B in 2 registri, sommare e porre il
risultato in un terzo registro e da lì copiarlo in C. Già dalla
seconda generazione di calcolatori sono stati perciò scritti dei
programmi - i
compilatori - in grado di tradurre un
programma
sorgente, scritto in un linguaggio mnemonico di livello più alto
dell'assembly e tale da esprimere concisamente certe operazioni specifiche, in
un
programma oggetto. Una volta compilato, il nostro programma può
essere direttamente eseguito dal calcolatore senza altri intermediari; se
prevediamo di usarlo più volte, potremo memorizzarlo ed in seguito
ricaricarlo senza passare ancora per il compilatore. Un tipo particolare di
compilatore è l'
interprete, che traduce ed esegue subito ogni
singola istruzione: da un lato ci permette quindi di verificare gli effetti di
un programma durante la stesura; dall'altro la continua traduzione rallenta
l'esecuzione. Esistono poi
interpreti/compilatori con cui dapprima si
mette a punto il programma e poi lo si compila così da ottenere la
massima velocità. Una tecnica di programmazione molto usata è la
procedura, in varie forme: si tratta di una sequenza di istruzioni a cui
possiamo riferirci con un nome -
chiamata di procedura - passando dei
valori -
argomenti - così da eseguire la sequenza senza doverla
riscrivere. Il programma viene suddiviso in procedure, e dato che ognuna di esse
ha un nome significativo, noi possiamo vedere a colpo d'occhio l'azione di tutto
il programma. Una procedura che abbia un solo risultato e non modifichi nessun
argomento è una
funzione matematica; ovviamente esistono anche
procedure senza valori di ritorno o che ritornano più valori alterando i
loro argomenti. Nella sequenza di istruzioni di una procedura può anche
trovarsi una
chiamata ricorsiva: si tratta di un nuovo riferimento alla
nostra procedura, ma con argomenti differenti; in altre parole vi sono 2 copie
della procedura in esecuzione: la prima ha chiamato la seconda e ne attende il
ritorno prima di proseguire. La
ricorsività è una tecnica
per operare su dati differenti conservando un legame fra di loro, costituito
dalle diverse copie della procedura. L'
iterazione è un'altra
tecnica per eseguire un'operazione più volte, ma senza mantenere legami
fra i dati, cioè con una sola copia della procedura: un'
istruzione di
ciclo racchiude una sequenza di istruzioni e specifica la condizione per
rieseguirla (V. esempio più avanti).
Generalmente la redazione di un programma inizia con la stesura di uno
schema
a blocchi o
flow chart, nella quale le operazioni logiche che il
calcolatore deve eseguire sono redatte in
pseudo-linguaggio nella loro
successione logica. Gli schemi a blocchi sono indipendenti dal linguaggio
simbolico usato e naturalmente anche dal calcolatore che deve eseguire il
programma. I due tipi principali di blocchi sono i seguenti: a)
blocco
imperativo, indicato col
simbolo

impone al calcolatore di
eseguire una certa operazione; può avere più di un ingresso ma ha
una sola uscita, e precisamente quella che porta all'istruzione successiva. b)
blocco di test, indicato col
simbolo

ha uno o più ingressi ma
sempre due uscite. In questo blocco si pone al calcolatore una domanda, alla
quale esso può rispondere SI o NO; secondo la risposta che dà alla
domanda il calcolatore passa a eseguire l'istruzione posta sull'uscita SI o
sull'uscita NO. Si noti che non è possibile porre domande che richiedano
una risposta diversa da queste e che le domande devono essere formulate in
termini matematici ben precisi. Altri blocchi di forma circolare o ellittica
vengono usati per indicare la lettura di dati o le operazioni di inizio e fine
dei calcoli. Come esempio di schema blocchi, si voglia formulare di trovare il
massimo di un certo numero di insiemi. Il procedimento usato in questi casi
è quello di assumere il primo numero come massimo (max) e di confrontarlo
col secondo. Il maggiore dei due viene confrontato col terzo. Il maggiore fra
questi viene confrontato col quarto, e così via fino all'esaurimento dei
numeri. Lo schema a blocchi di queste operazioni sarà il
seguente:

In forma più matematica
potremmo considerare i numeri dati come un insieme di N numeri costruenti una
successione a
1, a
2, ..., a
n; siccome la
tastiera del calcolatore non fornisce gli indici, diremo A(1), A(2), ..., A(N),
e indicheremo con A(I) il termine generico. Lo stesso schema a blocchi si
scriverà allora in questo
modo:

Si noti che all'inizio
dell'operazione compare un'istruzione che dice di leggere quanti sono i numeri,
e poi di leggere il termine generico A(I) e di classificarlo al variare
dell'indice; in tal modo si ottiene la lettura ordinata della successione. Si
hanno poi delle istruzioni circa il trasferimento di numeri e la variazione di
indici. Ad esempio l'istruzione: A(1) ¾ MAX significa "prendi il numero
contenuto in A(1), e assegnalo alla variabile MAX", mentre l'istruzione I + 1
→ I (ovvero I = I + 1) significa "aumenta di uno
l'indice del termine generico" ovvero "prendi il numero successivo". Successivi
sviluppi nei linguaggi -
programmazione strutturata - hanno introdotto
nuove tecniche per la rappresentazione dell'organizzazione del programma, come
gli
alberi algoritmici, in cui un programma è visto a blocchi
scomposti su diversi livelli con sempre maggior dettaglio, fino ad arrivare alle
effettive istruzioni -
programmazione top-down. Comunque si è
visto che, oltre a un linguaggio strutturato, la creazione di programmi
complessi richiede un ambiente che ne semplifichi ed automatizzi l'aggiornamento
-
mantenimento -, consentendo il passaggio biunivoco fra un progetto
simbolico ed il codice di programma. ║
Linguaggi simbolici: a
questo punto per dare una formulazione definitiva a un problema di qualsiasi
genere, occorre scegliere il linguaggio simbolico. Fra le diverse categorie di
linguaggi comprendiamo: gli
imperativi (procedurali), che permettono di
esprimere un compito come una sequenza di chiamate di procedura che manipolano
delle variabili; i
funzionali (applicativi, logici), con cui definiamo
delle funzioni da applicare a un insieme di dati (
dominio), dai quali
ricavare un risultato (
codominio) - notate che l'iterazione sui dati
è implicita: i linguaggi funzionali non hanno istruzioni di ciclo -; gli
object-oriented, con cui simuliamo concetti e fenomeni di un sistema
reale o immaginario. Un linguaggio è
puro se rientra in una sola
categoria; in effetti, i linguaggi più diffusi riuniscono caratteristiche
di varie categorie. Vediamo ora brevemente i principali, con la categoria di
appartenenza. a)
Linguaggio FORTRAN (Imperativo). Il nome deriva
dall'abbreviazione delle parole inglesi FOR
mula TRAN
slation
cioè "traduzione di formule". È particolarmente indicato a
esprimere problemi tecnici e scientifici, è disponibile su grandi e
piccoli calcolatori ed ha avuto parecchie versioni, dal FORTRAN IV degli anni
'50 al FORTRAN 90. b)
Linguaggio COBOL (Imperativo). Il nome deriva
dall'abbreviazione delle parole inglesi CO
mmon
B
usiness-O
riented L
anguage, cioè "linguaggio comune
orientato agli affari". È un linguaggio creato esplicitamente per
esprimere in maniera semplice i problemi commerciali e amministrativi che
richiedono generalmente una serie limitata di calcoli su una grande massa di
dati. Nacque negli USA nel 1960 ad opera di rappresentanti di case costruttrici
di calcolatori e dell'amministrazione statale. Esso si è rapidamente
diffuso ed è ancora usato soprattutto a causa dell'enorme mole di codice
esistente. c)
Linguaggio LISP (Funzionale). Il nome deriva
dall'abbreviazione delle parole inglesi LI
St Processor cioè
"elaboratore di liste". Fu creato nel 1958 da John McCarthy, visionario
professore del
MIT, per le ricerche sull'
intelligenza artificiale,
ed è infatti usato per l'implementazione di
sistemi esperti e la
comprensione del linguaggio naturale. Programmi e dati sono
indistintamente rappresentati come liste e ciò ne permette la
costruzione, valutazione e modifica durante l'esecuzione. d)
Linguaggio Smalltalk (Object-Oriented). Il nome significa piccolo
dialetto ed indica la semplicità del linguaggio e delle idee su cui si
basa. Concepito nei primi anni '70 e successivamente aggiornato più
volte, fa leva sui concetti di
oggetto, classe, istanza, metodo,
messaggio per costruire un intero
ambiente operativo autosufficiente
e modificabile al volo secondo necessità. Per le sue caratteristiche
dinamiche ha bisogno di velocità e memoria, e questo ha inizialmente
frenato la sua diffusione. e)
Linguaggio Pascal (Imperativo). Il nome
è stato dato da Niklaus Wirth, creatore del linguaggio, in onore di
Blaise Pascal, il filosofo francese del '600. Fu sviluppato nel 1970 come
strumento pedagogico per l'insegnamento della programmazione - e come tale
veniva usato nei corsi - e con gli anni è stato ampliato per l'uso in
programmi commerciali. Praticamente è disponibile su ogni calcolatore,
data la semplicità di implementazione dei compilatori Pascal, e per
questo ne vedremo ora più in dettaglio la definizione. I simboli vengono
distinti in
operatori, parole riservate, identificatori, numeri, stringhe di
caratteri. Gli operatori sono utilizzati nelle espressioni logiche,
aritmetiche, di assegnamento e di confronto:
+, -, *, /, DIV, MOD, NOT, AND,
OR, =, <, >, <>, <=, >=, IN, :=; il loro significato
è facilmente intuibile, a parte: IN (appartenenza a insieme), DIV
(divisione intera), MOD (modulo), <> (diverso da), * (prodotto), :=
(assegnamento). Le parole riservate e gli identificatori iniziano per lettera da
a a z e proseguono con lettere - per le parole riservate - ed anche cifre
- per gli identificatori. Un numero è composto di cifre da
0 a 9,
con possibilmente un segno (
+-), un punto (.) che fa da virgola per i
numeri reali, una
E per la
notazione esponenziale. Una stringa
è una sequenza di lettere, cifre e punteggiatura racchiusa fra apici (').
Il programma Pascal completo è diviso in 6
parti dichiarative, in
stretto ordine, di cui le prime 5 facoltative:
etichette, costanti, tipi,
variabili, procedure e funzioni, istruzioni eseguibili. Ogni elemento usato
deve essere prima dichiarato nella parte corrispondente; ad esempio, se nel
nostro programma usiamo un'etichetta, dobbiamo includere la parte dichiarativa
delle etichette. Per concludere l'esempio sopra trattato, vediamo come tradurre
lo schema a blocchi in un programma. Supponiamo che il numero massimo di numeri
da sommare con questo programma sia 1.000, e che i numeri siano reali,
cioè con la virgola. Il programma per questo calcolo è il
seguente:
PROGRAM
TrovaMassimo;
CONST dimensione =
1000;
VAR numeri: ARRAY [1..dimensione] OF
REAL;
ix:
INTEGER;
max:
REAL;
BEGIN
FOR ix
:= 1 TO dimensione DO READLN(numeri[Ix]);
max :=
numeri[1];
FOR ix := 2 TO dimensione DO IF max
< numeri[Ix] THEN max :=
numeri[Ix];
WRITELN(max);
END.
Ogni
programma Pascal inizia con un'istruzione PROGRAM che ne dichiara il nome, a
scopo di documentazione. Per il numero di elementi usiamo la costante
dimensione che rende evidente il significato delle istruzioni seguenti.
numeri può contenere 1000 numeri reali, a cui si accede con un
indice da 1 a 1000;
ix è una semplice variabile di ciclo;
max conterrà il massimo elemento - notate che è dello
stesso tipo di
numeri. Il blocco delle istruzioni eseguibili è
racchiuso dalle parole riservate BEGIN ed END. Il primo ciclo FOR assegna a
ix i valori da 1 a 1000, e per ogni valore esegue READLN, che legge dalla
tastiera un numero reale e lo pone in
numeri[Ix], vale a dire in
numeri[1],
...,
numeri[1000]. Poi a
max viene
assegnato il primo valore di
numeri, come inizio. Il secondo ciclo FOR
assegna a
ix i valori da 2 a 1000 - poiché
numeri[1]
è già in
max - e per ogni iterazione esegue IF, che
controlla se
max è minore di
numeri[Ix] e, se così,
assegna a
max il nuovo massimo. Infine, WRITELN stampa su schermo il
valore di
max. Naturalmente questo esempio è veramente semplice;
problemi più complessi vengono trattati scomponendoli in sotto-problemi
fino a giungere al livello del nostro esempio - nell'ipotesi migliore -
così che una sola persona possa comprenderne lo svolgimento. Tale
approccio si chiama "dividi e conquista", e può essere praticato con ogni
linguaggio, anche se con minore - in Pascal - o maggiore - in FORTRAN -
difficoltà.